from typing import Union, Tuple, Callable, Any

import requests
import urllib3

from requests.auth import HTTPBasicAuth
from requests.cookies import RequestsCookieJar

urllib3.disable_warnings()


class HttpContext(object):
    _DEFAULT_UA_CHROME = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0)' \
                ' AppleWebKit/535.11 (KHTML, like Gecko)' \
                ' Chrome/17.0.963.56 Safari/535.11'

    _DEFAULT_HEADERS = {
        'User-Agent': _DEFAULT_UA_CHROME,
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    def __init__(self, cookies_context: Any = None, header: Union[None, dict] = None):
        if isinstance(cookies_context, HttpContext):
            self._m_cookies: Union[None, RequestsCookieJar] = cookies_context._m_cookies
            self._m_header = {**cookies_context._m_header}
        else:
            if isinstance(cookies_context, RequestsCookieJar):
                self._m_cookies: Union[None, RequestsCookieJar] = cookies_context
            else:
                self._m_cookies: Union[None, RequestsCookieJar] = None

            if header:
                self._m_header = {**HttpContext._DEFAULT_HEADERS, **header}
            else:
                self._m_header = {**HttpContext._DEFAULT_HEADERS}

    def get_cookies(self) -> Union[None, RequestsCookieJar]:
        return self._m_cookies

    def set_cookies(self, cookies: Union[None, RequestsCookieJar]):
        self._m_cookies = cookies

    def get_header(self) -> Union[None, dict]:
        return self._m_header

    def set_header(self, key: str, val: str):
        self._m_header[key] = val


class Http(object):
    _ENCODING_LIST = (
        'ascii',
        'gbk',
        'utf8',
        'utf16'
    )

    DOWNLOAD_BUFFER_SIZE = 512 * 1024

    def __init__(self, username: str = None, password: str = None):
        self._m_auth: HTTPBasicAuth = HTTPBasicAuth(username, password)

    def get(self,
            url: str,
            context: Union[None, HttpContext] = None) -> Tuple[int, str]:
        response = requests.get(url=url,
                                verify=False,
                                auth=self._m_auth,
                                stream=True,
                                headers=context.get_header(),
                                cookies=context.get_cookies() if context else None)
        if response.status_code != 200:
            return response.status_code, Http._decode(response.content)

        # save cookies
        if len(response.cookies.items()) > 0 and context is not None:
            context.set_cookies(response.cookies)

        # get content
        content: bytes = b''
        for chunk in response.iter_content(self.DOWNLOAD_BUFFER_SIZE):
            content += chunk
        return response.status_code, Http._decode(content)

    def post(self,
             url: str,
             data: Union[None, bytes] = None,
             context: Union[None, HttpContext] = None) -> Tuple[int, str]:
        response = requests.post(url=url,
                                 data=data,
                                 verify=False,
                                 auth=self._m_auth,
                                 stream=True,
                                 headers=context.get_header(),
                                 cookies=context.get_cookies() if context else None)
        if response.status_code != 200:
            return response.status_code, Http._decode(response.content)

        # save cookies
        if len(response.cookies.items()) > 0 and context is not None:
            context.set_cookies(response.cookies)

        # get content
        content: bytes = b''
        for chunk in response.iter_content(self.DOWNLOAD_BUFFER_SIZE):
            content += chunk
        return response.status_code, Http._decode(content)

    def download(self,
                 url: str,
                 data: Union[None, bytes] = None,
                 on_receive: Union[None, Callable[[int, int, bytes], None]] = None,
                 context: Union[None, HttpContext] = None) -> Tuple[int, str]:
        if data:
            response = requests.post(url=url,
                                     data=data,
                                     verify=False,
                                     auth=self._m_auth,
                                     stream=True,
                                     headers=context.get_header(),
                                     cookies=context.get_cookies() if context else None)
        else:
            response = requests.get(url=url,
                                    verify=False,
                                    auth=self._m_auth,
                                    stream=True,
                                    headers=context.get_header(),
                                    cookies=context.get_cookies() if context else None)

        # exit for failed
        if response.status_code != 200:
            return response.status_code, Http._decode(response.content)

        # save cookies
        if len(response.cookies.items()) > 0 and context is not None:
            context.set_cookies(response.cookies)

        # get length
        if 'content-length' in response.headers:
            file_size = response.headers['content-length'].strip()
            if file_size == '':
                file_size = -1
            else:
                file_size = int(file_size)
        else:
            file_size = -1  # size unknown

        # check on_receive
        if on_receive is None:
            return response.status_code, ''

        # callback on_receive
        # update progress for start
        size_downloaded: int = 0
        on_receive(size_downloaded, file_size, b'')

        # do download
        for chunk in response.iter_content(self.DOWNLOAD_BUFFER_SIZE):
            size_downloaded += len(chunk)
            on_receive(size_downloaded, file_size, chunk)

        # update progress for finished
        on_receive(size_downloaded, file_size, b'')

        return response.status_code, ''

    def size(self, url: str, context: Union[None, HttpContext] = None) -> Tuple[int, int]:
        response = requests.get(url=url,
                                verify=False,
                                auth=self._m_auth,
                                stream=True,
                                headers=context.get_header(),
                                cookies=context.get_cookies() if context else None)
        if response.status_code != 200:
            return response.status_code, -1

        # save cookies
        if len(response.cookies.items()) > 0 and context is not None:
            context.set_cookies(response.cookies)

        # get length
        if 'content-length' in response.headers:
            file_size = response.headers['content-length'].strip()
            if file_size == '':
                file_size = -1
            else:
                file_size = int(file_size)
        else:
            file_size = -1  # size unknown

        response.close()
        return response.status_code, file_size

    @staticmethod
    def _decode(s: Union[None, bytes]) -> Union[None, str]:
        if s is None:
            return None

        for encoding in Http._ENCODING_LIST:
            try:
                return s.decode(encoding)
            except UnicodeDecodeError:
                pass
        return None
